package webx

import (
	"crypto/md5"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"gitee.com/xungen/goweb/errno"
	"gitee.com/xungen/goweb/utils"
)

var filemap = utils.NewCacheMap(1000, 3*time.Second)

type FileItem struct {
	size    int64
	utime   int64
	version string
	contype string
	content []byte
}

func GetFileItem(path string) *FileItem {
	if res, ok := filemap.Get(path); ok {
		return res.(*FileItem)
	} else if res == nil {
		var item FileItem
		if item.check(path) {
			filemap.Set(path, &item)
			return &item
		}
	} else {
		var file = res.(*FileItem)
		if item := *file; item.check(path) {
			filemap.Set(path, &item)
			return &item
		}
	}
	return nil
}

func Publish(src, dest string) error {
	info, err := os.Stat(src)
	if err != nil {
		return err
	}

	resource := func(path string, response http.ResponseWriter, request *http.Request) {
		if file := GetFileItem(path); file == nil {
			http.NotFoundHandler().ServeHTTP(response, request)
		} else {
			header := response.Header()
			header.Add("ETag", file.version)
			header.Add("Content-Type", file.contype)
			etag := request.Header.Get("If-None-Match")
			if etag == file.version {
				response.WriteHeader(304)
			} else {
				if strings.HasSuffix(path, ".gzip") {
					header.Add("Content-Encoding", "gzip")
				}
				file.write(path, response)
			}
		}
	}

	folder := func(response http.ResponseWriter, request *http.Request) {
		if path := request.URL.Path; strings.Contains(path, "..") || strings.ContainsAny(path, "$?*") {
			errno.PARAMERR.Panic("resource path error")
		} else if strings.HasPrefix(path, dest) {
			path = src + "/" + path[len(dest):]
			resource(path, response, request)
		} else {
			http.NotFoundHandler().ServeHTTP(response, request)
		}
	}

	if info.IsDir() {
		dest = "/" + dest + "/"
		dest = strings.ReplaceAll(dest, "\\", "/")
		dest = strings.ReplaceAll(dest, "//", "/")
		httpHandle(dest, folder)
	} else {
		httpHandle(FormatPath(dest), func(response http.ResponseWriter, request *http.Request) {
			resource(src, response, request)
		})
	}
	return nil
}

func PublishZip(src, dest string) error {
	if buff, err := os.ReadFile(src); err == nil {
		return publishZipBuffer(buff, dest)
	} else {
		return err
	}
}

func publishZipBuffer(buffer []byte, dest string) error {
	zipmap, err := utils.UnpackZipBuffer(buffer)
	if err != nil {
		return err
	}

	etag := fmt.Sprintf("%x", md5.Sum(buffer))
	resource := func(path string, response http.ResponseWriter, request *http.Request) {
		if buff, ok := zipmap[path]; ok {
			contype := GetContentType(filepath.Ext(path))
			header := response.Header()
			header.Add("ETag", etag)
			header.Add("Content-Type", contype)
			if etag == request.Header.Get("If-None-Match") {
				response.WriteHeader(304)
			} else {
				if strings.HasSuffix(path, ".gzip") {
					header.Add("Content-Encoding", "gzip")
				}
				errno.AssertNoError(writeAll(response, buff))
			}
		} else {
			http.NotFoundHandler().ServeHTTP(response, request)
		}
	}

	dest = "/" + dest + "/"
	dest = strings.ReplaceAll(dest, "//", "/")
	httpHandle(dest, func(response http.ResponseWriter, request *http.Request) {
		path := request.URL.Path
		if strings.Contains(path, "..") || strings.ContainsAny(path, "$?*") {
			errno.PARAMERR.Panic("resource path error")
		}
		if strings.HasPrefix(path, dest) {
			resource(path[len(dest):], response, request)
		} else {
			http.NotFoundHandler().ServeHTTP(response, request)
		}
	})
	return nil
}

func httpHandle(pattern string, handler func(http.ResponseWriter, *http.Request)) {
	http.HandleFunc(pattern, func(writer http.ResponseWriter, request *http.Request) {
		handler(writer, request)
	})
}

func (file *FileItem) check(path string) bool {
	if info, err := os.Stat(path); err == nil {
		var size = info.Size()
		var utime = info.ModTime().Unix()
		if size == file.size && utime == file.utime {
			return true
		}
		if size > 1024*1024 {
			file.content = nil
		} else if size == 0 {
			file.content = make([]byte, 0)
		} else {
			file.content = errno.AssertNoError(os.ReadFile(path))
		}
		file.size = size
		file.utime = utime
		file.version = fmt.Sprintf("%d:%d", utime, size)
		file.contype = GetContentType(filepath.Ext(path))
		return true
	}
	return false
}

func (file *FileItem) write(path string, writer io.Writer) {
	if file.content == nil {
		num := int64(0)
		buff := make([]byte, 0xFFFF)
		reader := errno.AssertNoError(os.Open(path))
		defer reader.Close()
		for num < file.size {
			res, err := reader.Read(buff)
			if res > 0 {
				errno.AssertNoError(writeAll(writer, buff[:res]))
			}
			if err == io.EOF {
				break
			}
			errno.AssertNil(err)
			num += int64(res)
		}
	} else {
		errno.AssertNoError(writeAll(writer, file.content))
	}
}
